package drds.connection_pool.proxy;

import drds.connection_pool.PoolEntry;
import drds.connection_pool.ProxyFactory;
import drds.connection_pool.ProxyLeakRunnable;
import drds.connection_pool.util.FastList;
import drds.connection_pool.util.clock.Clock;
import lombok.extern.slf4j.Slf4j;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.sql.*;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.Executor;

@Slf4j
public abstract class ConnectionProxy implements Connection
{
   public static final int DIRTY_BIT_READONLY = 0b000001;
   public static final int DIRTY_BIT_AUTOCOMMIT = 0b000010;
   public static final int DIRTY_BIT_ISOLATION = 0b000100;
   public static final int DIRTY_BIT_CATALOG = 0b001000;
   public static final int DIRTY_BIT_NETTIMEOUT = 0b010000;
   public static final int DIRTY_BIT_SCHEMA = 0b100000;
   //
   private static final Set<String> ERROR_STATES;
   private static final Set<Integer> ERROR_CODES;

   // static initializer
   static
   {


      ERROR_STATES = new HashSet<>();
      ERROR_STATES.add("0A000"); // FEATURE UNSUPPORTED
      ERROR_STATES.add("57P01"); // ADMIN SHUTDOWN
      ERROR_STATES.add("57P02"); // CRASH SHUTDOWN
      ERROR_STATES.add("57P03"); // CANNOT CONNECT NOW
      ERROR_STATES.add("01002"); // SQL92 disconnect error
      ERROR_STATES.add("JZ0C0"); // Sybase disconnect error
      ERROR_STATES.add("JZ0C1"); // Sybase disconnect error

      ERROR_CODES = new HashSet<>();
      ERROR_CODES.add(500150);
      ERROR_CODES.add(2399);
   }

   private final PoolEntry poolEntry;
   private final ProxyLeakRunnable proxyLeakRunnable;
   private final FastList<Statement> statementList;
   @SuppressWarnings("WeakerAccess")
   protected Connection delegate;
   private int dirtyBits;
   private long lastAccess;
   private boolean isCommitStateDirty;
   private boolean isReadOnly;
   private boolean isAutoCommit;
   private int networkTimeout;
   private int transactionIsolation;
   private String dbcatalog;
   private String dbschema;

   protected ConnectionProxy(final PoolEntry poolEntry, final Connection connection, final FastList<Statement> statementList, final ProxyLeakRunnable proxyLeakRunnable, final long now, final boolean isReadOnly, final boolean isAutoCommit)
   {
      this.poolEntry = poolEntry;
      this.delegate = connection;
      this.statementList = statementList;
      this.proxyLeakRunnable = proxyLeakRunnable;
      this.lastAccess = now;
      this.isReadOnly = isReadOnly;
      this.isAutoCommit = isAutoCommit;
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public final String toString()
   {
      return this.getClass().getSimpleName() + '@' + System.identityHashCode(this) + " wrapping " + delegate;
   }

   // ***********************************************************************
   //                     Connection State Accessors
   // ***********************************************************************

   public final boolean getAutoCommitState()
   {
      return isAutoCommit;
   }

   public final String getCatalogState()
   {
      return dbcatalog;
   }

   public final String getSchemaState()
   {
      return dbschema;
   }

   public final int getTransactionIsolationState()
   {
      return transactionIsolation;
   }

   public final boolean getReadOnlyState()
   {
      return isReadOnly;
   }

   public final int getNetworkTimeoutState()
   {
      return networkTimeout;
   }

   // ***********************************************************************
   //                          Internal methods
   // ***********************************************************************

   public final PoolEntry getPoolEntry()
   {
      return poolEntry;
   }

   final SQLException checkException(SQLException sqle)
   {
      SQLException nse = sqle;
      for (int depth = 0; delegate != ClosedConnection.CLOSED_CONNECTION && nse != null && depth < 10; depth++)
      {
         final String sqlState = nse.getSQLState();
         if (sqlState != null && sqlState.startsWith("08") || ERROR_STATES.contains(sqlState) || ERROR_CODES.contains(nse.getErrorCode()))
         {
            // broken connectionProxy
            log.warn("{} - Connection {} marked as broken because of SQLSTATE({}), ErrorCode({})",
               poolEntry.getPoolName(), delegate, sqlState, nse.getErrorCode(), nse);
            proxyLeakRunnable.cancel();
            poolEntry.evict("(connectionProxy is broken)");
            delegate = ClosedConnection.CLOSED_CONNECTION;
         } else
         {
            nse = nse.getNextException();
         }
      }

      return sqle;
   }

   final synchronized void untrackStatement(final Statement statement)
   {
      statementList.remove(statement);
   }

   final void markCommitStateDirty()
   {
      if (isAutoCommit)
      {
         lastAccess = Clock.currentTime();
      } else
      {
         isCommitStateDirty = true;
      }
   }

   public void cancelLeakTask()
   {
      proxyLeakRunnable.cancel();
   }

   private synchronized <Statement extends java.sql.Statement> Statement addStatement(final Statement statement)
   {
      statementList.add(statement);
      return statement;
   }

   @SuppressWarnings("EmptyTryBlock")
   private synchronized void closeStatements()
   {
      final int size = statementList.size();
      if (size > 0)
      {
         for (int i = 0; i < size && delegate != ClosedConnection.CLOSED_CONNECTION; i++)
         {
            try (Statement ignored = statementList.get(i))
            {
               // automatic resource cleanup
            } catch (SQLException e)
            {
               log.warn("{} - Connection {} marked as broken because of an exception closing open statements during Connection.close()",
                  poolEntry.getPoolName(), delegate);
               proxyLeakRunnable.cancel();
               poolEntry.evict("(exception closing Statements during Connection.close())");
               delegate = ClosedConnection.CLOSED_CONNECTION;
            }
         }

         statementList.clear();
      }
   }

   // **********************************************************************
   //              "Overridden" java.sql.Connection Methods
   // **********************************************************************

   /**
    * {@inheritDoc}
    */
   @Override
   public final void close() throws SQLException
   {
      // Closing statements can cause connectionProxy eviction, so this must run before the conditional below
      closeStatements();

      if (delegate != ClosedConnection.CLOSED_CONNECTION)
      {
         proxyLeakRunnable.cancel();

         try
         {
            if (isCommitStateDirty && !isAutoCommit)
            {
               delegate.rollback();
               lastAccess = Clock.currentTime();
               log.debug("{} - Executed rollback on connectionProxy {} due to dirty commit state on close().", poolEntry.getPoolName(), delegate);
            }

            if (dirtyBits != 0)
            {
               poolEntry.resetConnectionState(this, dirtyBits);
               lastAccess = Clock.currentTime();
            }

            delegate.clearWarnings();
         } catch (SQLException e)
         {
            // when connections are aborted, exceptions are often thrown that should not reach the application
            if (!poolEntry.isMarkedEvicted())
            {
               throw checkException(e);
            }
         } finally
         {
            delegate = ClosedConnection.CLOSED_CONNECTION;
            poolEntry.recycle(lastAccess);
         }
      }
   }

   /**
    * {@inheritDoc}
    */
   @Override
   @SuppressWarnings("RedundantThrows")
   public boolean isClosed() throws SQLException
   {
      return (delegate == ClosedConnection.CLOSED_CONNECTION);
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public Statement createStatement() throws SQLException
   {
      return ProxyFactory.getStatementProxy(this, addStatement(delegate.createStatement()));
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public Statement createStatement(int resultSetType, int concurrency) throws SQLException
   {
      return ProxyFactory.getStatementProxy(this, addStatement(delegate.createStatement(resultSetType, concurrency)));
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public Statement createStatement(int resultSetType, int concurrency, int holdability) throws SQLException
   {
      return ProxyFactory.getStatementProxy(this, addStatement(delegate.createStatement(resultSetType, concurrency, holdability)));
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public CallableStatement prepareCall(String sql) throws SQLException
   {
      throw new UnsupportedOperationException();
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public CallableStatement prepareCall(String sql, int resultSetType, int concurrency) throws SQLException
   {
      throw new UnsupportedOperationException();
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public CallableStatement prepareCall(String sql, int resultSetType, int concurrency, int holdability) throws SQLException
   {
      throw new UnsupportedOperationException();
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public PreparedStatement prepareStatement(String sql) throws SQLException
   {
      return ProxyFactory.getPreparedStatementProxy(this, addStatement(delegate.prepareStatement(sql)));
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException
   {
      return ProxyFactory.getPreparedStatementProxy(this, addStatement(delegate.prepareStatement(sql, autoGeneratedKeys)));
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public PreparedStatement prepareStatement(String sql, int resultSetType, int concurrency) throws SQLException
   {
      return ProxyFactory.getPreparedStatementProxy(this, addStatement(delegate.prepareStatement(sql, resultSetType, concurrency)));
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public PreparedStatement prepareStatement(String sql, int resultSetType, int concurrency, int holdability) throws SQLException
   {
      return ProxyFactory.getPreparedStatementProxy(this, addStatement(delegate.prepareStatement(sql, resultSetType, concurrency, holdability)));
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException
   {
      return ProxyFactory.getPreparedStatementProxy(this, addStatement(delegate.prepareStatement(sql, columnIndexes)));
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException
   {
      return ProxyFactory.getPreparedStatementProxy(this, addStatement(delegate.prepareStatement(sql, columnNames)));
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public DatabaseMetaData getMetaData() throws SQLException
   {
      markCommitStateDirty();
      return delegate.getMetaData();
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public void commit() throws SQLException
   {
      delegate.commit();
      isCommitStateDirty = false;
      lastAccess = Clock.currentTime();
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public void rollback() throws SQLException
   {
      delegate.rollback();
      isCommitStateDirty = false;
      lastAccess = Clock.currentTime();
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public void rollback(Savepoint savepoint) throws SQLException
   {
      delegate.rollback(savepoint);
      isCommitStateDirty = false;
      lastAccess = Clock.currentTime();
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public void setAutoCommit(boolean autoCommit) throws SQLException
   {
      delegate.setAutoCommit(autoCommit);
      isAutoCommit = autoCommit;
      dirtyBits |= DIRTY_BIT_AUTOCOMMIT;
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public void setReadOnly(boolean readOnly) throws SQLException
   {
      delegate.setReadOnly(readOnly);
      isReadOnly = readOnly;
      isCommitStateDirty = false;
      dirtyBits |= DIRTY_BIT_READONLY;
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public void setTransactionIsolation(int level) throws SQLException
   {
      delegate.setTransactionIsolation(level);
      transactionIsolation = level;
      dirtyBits |= DIRTY_BIT_ISOLATION;
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public void setCatalog(String catalog) throws SQLException
   {
      delegate.setCatalog(catalog);
      dbcatalog = catalog;
      dirtyBits |= DIRTY_BIT_CATALOG;
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException
   {
      delegate.setNetworkTimeout(executor, milliseconds);
      networkTimeout = milliseconds;
      dirtyBits |= DIRTY_BIT_NETTIMEOUT;
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public void setSchema(String schema) throws SQLException
   {
      delegate.setSchema(schema);
      dbschema = schema;
      dirtyBits |= DIRTY_BIT_SCHEMA;
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public final boolean isWrapperFor(Class<?> iface) throws SQLException
   {
      return iface.isInstance(delegate) || (delegate != null && delegate.isWrapperFor(iface));
   }

   /**
    * {@inheritDoc}
    */
   @Override
   @SuppressWarnings("unchecked")
   public final <T> T unwrap(Class<T> iface) throws SQLException
   {
      if (iface.isInstance(delegate))
      {
         return (T) delegate;
      } else if (delegate != null)
      {
         return delegate.unwrap(iface);
      }

      throw new SQLException("Wrapped connectionProxy is not an instance of " + iface);
   }

   // **********************************************************************
   //                         Private classes
   // **********************************************************************

   private static final class ClosedConnection
   {
      static final Connection CLOSED_CONNECTION = getClosedConnection();

      private static Connection getClosedConnection()
      {
         InvocationHandler handler = (proxy, method, args) ->
         {
            final String methodName = method.getName();
            if ("isClosed".equals(methodName))
            {
               return Boolean.TRUE;
            } else if ("isValid".equals(methodName))
            {
               return Boolean.FALSE;
            }
            if ("abort".equals(methodName))
            {
               return Void.TYPE;
            }
            if ("close".equals(methodName))
            {
               return Void.TYPE;
            } else if ("toString".equals(methodName))
            {
               return ClosedConnection.class.getCanonicalName();
            }

            throw new SQLException("Connection is closed");
         };

         return (Connection) Proxy.newProxyInstance(Connection.class.getClassLoader(), new Class[]{Connection.class}, handler);
      }
   }
}
